
/*
 * Pooler
 *
 * an object relations pool manager
 *
 */
(function(name, global, definition) {
    if(typeof module !== "undefined")
        module.exports = definition(name, global);
    else if(typeof define === "function" && typeof define.amd === "object")
        define(definition);
    else
        global[name] = definition(name, global);
}) ("pooler", this, function(name, global) {


    /**
     * pool class constructor
     * @param Number size
     * @param Number thresold
     */
    var Pooler = function(size, thresold) {

        // pool size (number of blocks)
        this.size = size > 0 ? size : 1000;

        // reallocation
        this.thresold = thresold ? thresold : this.size * 0.01;

        // effective maximum UID
        this.maximumUID = 0;

        // blocks list
        this.blocksList = [];

        // key lookup (key/UID pairs)
        this.keysLookup = {};

        // empty blocks UID list
        this.emptysList = [];

        // focus blocks UID list
        this.focusList = [];

        this.allocate();
    };

    /**
     * pool block object
     * @param Number UID
     * ---
     * duck should be one of
     * - empty      empty, not allocated block
     * - item       stored item
     * - define     link definition block
     * - link       actual link instance
     * - term       actual link term
     * 
     */
    var Block = function(UID) {
        this.UID = UID;
        this.type = "empty";
        this.key = "empty#" + UID;

        // data holded by block
        // this.value = null;

        //  "scout" related block UID
        this.scout = 0;

        // follower blocks UID list
        this.followersList = [];
    };

    /**
     * blocks allocation
     */
    Pooler.prototype.allocate = function() {

        for(i = 0; i < this.size; i++) {
            this.blocksList.push(new Block(this.maximumUID));
            this.emptysList.push(this.maximumUID);
            this.maximumUID++;
        }

        this.size *= 2;
        this.thresold *= 2;
    };


    /*
     *	duck a block to a given usage
     *
     * @param String type - one of "empty", "item", "define" or "link
     * @param String or null key
     * @param Object value
     * @param Boolean reset
     * 
     * @return Block the ducked block
     *
     * truth table :
     * !key && !reset --> new anonymous entry (link)
     * !key && reset --> throw error
     * key && !reset (+keyLookup) --> throw error
     *               --> else add new entry
     * key &&  reset --> reset existing entry
     */
    Pooler.prototype.duck = function(type, key, duck, reset) {
        var UID, existingUID, block;

        if(!key && reset)
            throw new Error("Couldn't reset anonymous block");

        existingUID = this.keysLookup[key];

        if(existingUID && !reset)
            throw new Error("key must be unique !");

        if(existingUID) {
            UID = this.dispose(existingUID);
        } else {
            if(this.emptysList.length < this.thresold)
                this.allocate();
            UID = this.emptysList.pop();
        }

        block = this.blocksList[UID];
        block.type = type;
        block.key = key;
        for(k in duck) {
            block[k] = duck[k];
        }

        if(key) this.keysLookup[key] = UID;

        return block;
    };

    /**
     * dispose a block
     * @param Number UID
     *
     */
    Pooler.prototype.dispose = function(UID) {
        var block = this.blocksList[UID];
        if(block && block.duck != "empty") {
            this.blocksList[UID] = new Block(UID);
            this.emptysList.push(UID);
            return UID;
        } else {
            throw new Error("Non existing, or yet disposed block at UID #" + UID);
        }
    };

    /**
     * store an item in the pool
     * @param String key
     * @param Object value
     * @param Boolean reset
     *
     */
    Pooler.prototype.store = function(key, item, reset) {
        return this.duck("item", key, { item : item}, reset);
    };

    /** 
     * lookup key generation from roles 
     * @param Object roles
     */
    Pooler.prototype.generateKey = function(roles) {
        var parts = [];
        for(r in roles) {
            r = r.toLowerCase();
            parts.push(r);
        }
        parts = parts.sort();
        return parts.join(":");
    };

    /**
     * define a relation in the pool
     * callbacks will be fired :
     * - onlink : when objects are connected
     * - onsplit : when a relation is splitted
     * - onprompt : when a link is prompted (get)
     */
    Pooler.prototype.define = function(roles, callbacks) {
        var UID, role;
        var key = this.generateKey(roles);

        if(this.keysLookup[key])
            throw new Error("Attempting to redefine relation !");

        for(r in roles) {
            role = roles[r];
            if( isNaN(role.limit) || role.limit < 0)
                throw new Error("role must have a numeric limit, even zero one !");
            role.getter = role.getter || r;
        }

        var ec, expectedCallbacks = ["onlink", "onget", "onsplit"];
        for(i = 0; i < expectedCallbacks.length; i++) {
            ec = expectedCallbacks[i];
            if( ! callbacks[ec])
                callbacks[ec] = function() {};
        }

        return this.duck("define", key, {
            roles : roles,
            callbacks : callbacks
        });
    };

    /**
     *  link object in the pool
     * @param Object actual
     * @param Object data
     */
    Pooler.prototype.link = function(actual, data) {
        // 1. check if there s a definintion for this link
        // console.log("link");

        var defKey = this.generateKey(actual);
        
        var defUID = this.keysLookup[defKey];
        if(defUID) {
            var def, link, terms = {};            // found a definition

            // 2a. create link block
            def = this.blocksList[defUID];
            link = this.duck(
                "link",
                null,
                { actual : actual, data : data }
            );
            //this.blocksList[linkUID].scout = defUID;
            link.scout = defUID;
            def.followersList.push(link.UID);

            // 2b. check if link participants exists
            //      and collect them
            var itemUID, itemKey;
            var participants = [];
            var term;
            
            for(role in actual) {
                itemKey = actual[role];
                itemUID = this.keysLookup[itemKey];
                
                if(itemUID) {
                    term = this.duck( "term", 
                        null,
                        {
                            itemKey : itemKey,
                            itemUID : itemUID,
                            role : role,
                            getter : def.roles[role].getter,
                            linkUID : link.UID
                        }
                    );
                    
                    term.scout = link.UID;
                    link.followersList.push(term.UID);
                    
                    // 2c. alter linked items       
                    item = this.blocksList[itemUID];
                    item.followersList.push(term.UID);
                    
                    // collect for onlink callback
                    terms[itemKey] = this.blocksList[term.itemUID].item;
                } else {
                    throw new Error("Not existing item at  key : " + itemKey);
                }
            }
            // console.log('item followers : ' + item.followersList.length);
            
            def.callbacks["onlink"].call(this, terms);            
            
            return link;
        } else {
            throw new Error("No definition found for actual roles");
        }
    };

    // break an existing link
    Pooler.prototype.split = function(oldies) {

    };

    // reset focus and select objects in the pool
    Pooler.prototype.focus = function(selector) {
       this.focusList = [];
       if(!selector) return this;

       // selectors
       // "|" perform a OR selection, if an item is missing, continue
       // "&" performs and AND selection, empty result on missing item

        if(selector.indexOf("|") != -1 && selector.indexOf("&") != -1)
            throw new Error("OR'n'AND selectors are mutuallt exclusive");

       if(selector.indexOf("|") != -1) {
            parts = selector.split("|");
            failOnMissing = false;

       } else if(selector.indexOf("&") != -1) {
            parts = selector.split("&");
            failOnMissing = true;
       } else {
            parts = [selector];
       }

       for(i = 0; i < parts.length; i++) {
            key = parts[i].replace("\s+\S+", "");
            UID = this.keysLookup[key];

            if(UID) {
                this.focusList.push(UID);
            } else {
                if(failOnMissing) {
                    this.focusList = [];
                    break;
                }
            }
            return this;
       }

    };

    // loop through focused items/relations and apply callback
    Pooler.prototype.each = function(callback) {
        var block;
        for(i = 0; i < focusList.length; i++) {
            block = this.blocksList[i];
            callback.call(this, block.value);
        }
    };

    // magic get !
    // if key is empty, return focused item(s)
    // if key is numeric, return focused item
    // if key is a string, step through related items of focused ones (merge)
    // if others is provided, add participants
    Pooler.prototype.get = function(key, others) {

        if(arguments.length == 0) {
            return this.getAllFocused();
        }

        if(typeof key == "number" && !isNaN(parseInt(key))) {
            return this.getItemFocusedAt(key);
        }

        // console.log(this.getFocusRelatedItems(key, others));
        return this.getFocusRelatedItems(key, others);
    };

    // get focused items
    Pooler.prototype.getAllFocused = function() {
        var result = [];
        var UID;
        for(i = 0; i < this.focusList.length; i++) {
            UID = this.focusList[i];
            block = this.blocksList[UID];
            result.push(block.item);
        }
        return result;
    };

    // get one fpcosed item at index
    Pooler.prototype.getItemFocusedAt = function(index) {
        var UID = this.focusList[index];
        return this.blocksList[UID].item;
    };

    // get related items
    Pooler.prototype.getFocusRelatedItems = function(getter, others) {
        var result = [];
        var otherParticipants = others || {};
        var item;

        for(i = 0; i < this.focusList.length; i++) {
            item = this.blocksList[this.focusList[i]];
            result = result.concat(this.getRelatedItems(item, getter, others));
        }
        return result;
    };

    // get related items for a given item
    Pooler.prototype.getRelatedItems = function(item, getter, others) {
        var result = [];
        var linkUID, link;
        var def;

        // console.log(item.followersList);
        // iterate through terms attached to this item
        // console.log("item.followersList.length " + +item.UID + " " + item.followersList.length);
        for(j = 0; j < item.followersList.length; j++) {
            
            termUID = item.followersList[j];
            term = this.blocksList[termUID];

            linkUID = term.scout;
            link = this.blocksList[linkUID];

            defUID = link.scout;
            def = this.blocksList[defUID];
          /*  
            console.log(" termUID: " + termUID 
                + " linkUID: " + linkUID
                + " defUID: " + defUID
            );
        */
            // console.log(getter + " tg : " + term.getter);
            // console.log(term);
            if(term.getter != getter) {
                var terms = {};
                var temp = [];    
                
                var relUID, relTerm, relItem;
                for(i = 0; i < link.followersList.length; i++) {
                    
                    relUID = link.followersList[i];
                    relTerm = this.blocksList[relUID];
                    relItem = this.blocksList[relTerm.itemUID];                    
                    
                    if(relTerm.getter == getter) {
                       // temp.push(relItem.item);
                       result.push(relItem.item);
                    }
                
                    // collect terms for callback
                    terms[relTerm.itemKey] = relItem.item;
                }
                
                // onget callback
                def.callbacks["onget"].call(this, terms, link.data);                
                // result = result.concat(temp);
            }
            
        }
        //console.log("result");
        // console.log(item.UID);
        // console.log(result);
        
        return result;
    };

    // reveal pool structure (debug)
    Pooler.prototype.reveal0 = function() {
        var result = {};
        var block, duck;
        for(i = 0, blen = this.blocksList.length; i < blen; i++) {
            block = this.blocksList[i];
            duck = block.duck;
            if(duck in result) {
                result[duck].push(block.UID);
            } else {
                result[duck] = [block.UID];
            };
        }

        result["_KEYS_LOOKUP_"] = [];
        for(k in this.keysLookup) {
            var s = "key: " + k + " " + "UID: " + this.keysLookup[k];
            result["_KEYS_LOOKUP_"].push(s);
        }

        result["_N_BLOCKS_"] = this.blocksList.length;

        return result;
    };

    Pooler.prototype.reveal = function() {
        var lines = [], line, parts, block;
        lines[0] = "revealing pool...";
        for(i = 0, blen = this.blocksList.length; i < blen; i++) {
            block = this.blocksList[i];
            parts = [];
            parts[0] = block.UID;
            parts[1] = block.type;
            parts[2] = "S: " + block.scout;
            parts[3] = "F: " 
                + (block.followersList.length 
                ? block.followersList.join(",") 
                : "<empty>");
                
            parts[4] = "\t" + "key:" + block.key;
            line = parts.join("\t");
            lines.push(line);
        }
        return lines.join("\n");
        
    }

    /**
    * factory method
    * @param Number size
    * @param Number thresold
    * @return Pooler object
    */
    var pooler = function(size, thresold) {

        var _pooler = new Pooler(size, thresold);

        var scopeFun = function(selector) {
            return _pooler.focus(selector);
        };

        scopeFun.store = function(key, value, reset) {
            return _pooler.store(key, value, reset);
        };

        scopeFun.define = function(roles, callbacks) {
            return _pooler.define(roles, callbacks);
        };

        scopeFun.link = function(actual, data) {
            return _pooler.link(actual, data);
        };

        scopeFun.split = function(actual) {
            return _pooler.split(actual);
        };

        scopeFun.each = function(callback) {
            return _pooler.each(callback);
        };


        scopeFun.reveal = function() {
            return _pooler.reveal();
        };

        return scopeFun;
    };

    return pooler;

});
